<?php

declare(strict_types=1);

namespace xpzhu\Jwt;

use Lcobucci\JWT\Token;
use xpzhu\Cache\Facade\Cache;

class Manager
{
    /**
     * 缓存前缀
     * @var string
     */
    protected string $prefix = 'xpzhu_jwt';

    /**
     * 黑名单缓存名
     * @var string
     */
    protected string $blacklist = 'blacklist';

    /**
     * 白名单缓存名
     * @var string
     */
    protected string $whitelist = 'whitelist';

    /**
     * @var Jwt
     */
    protected Jwt $jwt;

    /**
     * BlackList constructor.
     * @param Jwt $jwt
     */
    public function __construct(Jwt $jwt)
    {
        $options = config('plugin.xpzhu.jwt.app.manager') ?? [];

        if (!empty($options)) {
            foreach ($options as $key => $value) {
                $this->$key = $value;
            }
        }

        $this->jwt = $jwt;
    }

    /**
     * 登录
     * @param Token $token
     */
    public function login(Token $token): void
    {
        if ($this->jwt->getConfig()->getSsoEnable()) {
            $this->handleSSO($token);
        }
        $this->pushWhitelist($token);
    }

    /**
     * 单点登录处理
     * @param Token $token
     */
    protected function handleSSO(Token $token): void
    {
        $jti   = $token->claims()->get('jti');
        $store = $token->claims()->get('store');
        $this->destroyToken($jti, $store);
    }

    /**
     * 登出
     * @param Token $token
     */
    public function logout(Token $token): void
    {
        $type  = $this->getWhitelistName();
        $jti   = $token->claims()->get('jti');
        $store = $token->claims()->get('store');
        $key   = $this->makeKey($store, $type, $jti, $token);
        Cache::delete($key);

        $this->pushBlacklist($token);
    }

    /**
     * 加入白名单
     * @param Token $token
     */
    protected function pushWhitelist(Token $token): void
    {
        $jti   = $token->claims()->get('jti');
        $store = $token->claims()->get('store');

        $exp = $token->claims()->get('exp');
        $ttl = $exp->getTimestamp() - time();
        $tag = $store . '-' . $this->getWhitelistName();

        $key = $this->makeKey($store, $this->getWhitelistName(), $jti, $token);
        $this->setCache($tag, $key, $token, $ttl);
    }

    /**
     * 加入黑名单
     * @param Token $token
     */
    protected function pushBlacklist(Token $token): void
    {
        $jti   = $token->claims()->get('jti');
        $store = $token->claims()->get('store');

        $exp = $token->claims()->get('exp');
        $ttl = $this->jwt->getConfig()->getRefreshTtl();
        $exp = $exp->modify("+{$ttl} sec");
        $ttl = $exp->getTimestamp() - time();
        $tag = $store . '-' . $this->getBlacklistNamne();
        $key = $this->makeKey($store, $this->getBlacklistNamne(), $jti, $token);

        $this->setCache($tag, $key, $token, $ttl);
    }

    /**
     * 检测是否在黑名单
     * @param Token $token
     * @return bool
     */
    public function wasBan(Token $token): bool
    {
        return $this->getBlacklist($token) === $token->toString();
    }

    /**
     * 获取黑名单
     * @param Token $token
     * @return mixed
     */
    protected function getBlacklist(Token $token)
    {
        $jti   = $token->claims()->get('jti');
        $store = $token->claims()->get('store');
        return $this->getCache($store, $this->getBlacklistNamne(), $jti, $token);
    }

    /**
     * 注销指定应用下缓存的用户 （强制下线 重新登录）
     * @param string $store 应用名称
     * @return bool
     */
    public function destroyStoreWhitelist(string $store = ''): bool
    {
        empty($store) && $store = $this->jwt->getStore();
        return $this->clearStoreWhitelist($store);
    }

    /**
     * 注销指定应用下黑名单缓存
     * @param string $store 应用名称
     * @return bool
     */
    public function destroyStoreBlacklist(string $store = ''): bool
    {
        empty($store) && $store = $this->jwt->getStore();
        return $this->clearStoreBlacklist($store);
    }

    /**
     * 文件类型缓存解密
     * @param $filename
     * @return array|null
     */
    protected function decodeFileCache($filename)
    {
        $content = @file_get_contents($filename);
        if (false !== $content) {
            $expire  = (int)substr($content, 8, 12);
            $content = substr($content, 32);
            return is_string($content) ? ['content' => $content, 'expire' => $expire] : null;
        }
        return null;
    }

    /**
     * 注销指定某个用户（强制下线 重新登录）
     * @param $id
     * @param $store
     */
    public function destroyToken($id, $store): void
    {
        $type = $this->getWhitelistName();
        $tag  = $store . '-' . $type;
        $keys = Cache::getTagItems($tag);

        $config = config('plugin.xpzhu.cache.app');
        $handle = strtolower($config['default']);
        $prefix = $config['stores'][$handle]['prefix'];

        $prefix_len = mb_strlen($prefix);

        foreach ($keys as $key) {
            $token = null;
            if ($handle == 'file') {
                $token = unserialize($this->decodeFileCache($key)['content']);
            } else if ($handle == 'redis') {
                // 获取标签包含的缓存标识，自动加入缓存前缀。get获取缓存值也会自动加入缓存前缀，所以如果有缓存前缀，去掉
                $prefix_len && $key = substr($key, $prefix_len);
                $token = Cache::get($key);
            }

            if ($token) {
                $token = $this->jwt->parse($token);
                if ($token->claims()->has('jti') && $token->claims()->get('jti') == $id) {
                    $key = $this->makeKey($store, $type, $id, $token);
                    Cache::delete($key);
                    $this->pushBlacklist($token);
                }
            }
        }
    }

    /**
     * 生成缓存 Key
     * @param string $store
     * @param string $type
     * @param mixed  $uid
     * @param Token  $token
     * @return string
     */
    private function makeKey($store, $type, $uid, Token $token): string
    {
        $hash   = md5($token->toString());
        $prefix = $this->getPrefix();
        return implode(':', [$prefix, $store, $type, $uid, $hash]);
    }

    /**
     * 清除应用下白名单缓存
     * @param string $store 应用名称
     * @return bool
     */
    protected function clearStoreWhitelist(string $store): bool
    {
        return $this->clearTag($store . '-' . $this->getWhitelistName());
    }

    /**
     * 清除应用下黑名单缓存
     * @param string $store 应用名称
     * @return bool
     */
    protected function clearStoreBlacklist(string $store): bool
    {
        return $this->clearTag($store . '-' . $this->getBlacklistNamne());
    }

    private function clearTag($tag): bool
    {
        return Cache::tag($tag)->clear();
    }

    private function setCache($tag, $key, Token $token, $exp): bool
    {
        return Cache::tag($tag)->set($key, $token->toString(), $exp);
    }

    private function getCache($store, $type, $jti, $token)
    {
        $key = $this->makeKey($store, $type, $jti, $token);
        return Cache::get($key);
    }

    /**
     * 获取缓存前缀
     * @return string
     */
    private function getPrefix(): string
    {
        return $this->prefix;
    }

    /**
     * 获取黑名单缓存名
     * @return string
     */
    private function getBlacklistNamne(): string
    {
        return $this->blacklist;
    }

    /**
     * 获取白名单缓存名
     * @return string
     */
    private function getWhitelistName(): string
    {
        return $this->whitelist;
    }

}
